#!/usr/bin/env python3
"""
git_pull_all - 批量 Git 仓库更新工具
递归扫描指定目录下的所有子目录，识别 Git 仓库，并执行 git pull 命令更新所有仓库
"""

import sys
import os
from pathlib import Path
import argparse
import concurrent.futures
import json
import configparser
from typing import List, Dict, Any, Tuple, Optional

# 导入我们创建的模块
try:
    from models.git_repository import GitRepository
    from models.configuration import Configuration
    from utils.directory_scanner import DirectoryScanner
    from utils.git_executor import GitExecutor
    from utils.result_reporter import ResultReporter
except ImportError as e:
    print(f"错误：无法导入模块: {e}")
    print("请确保所有模块文件都在正确的位置")
    sys.exit(1)


def parse_args() -> argparse.Namespace:
    """
    解析命令行参数
    
    Returns:
        argparse.Namespace: 解析后的参数对象
    """
    parser = argparse.ArgumentParser(
        description='Git Pull All - 批量 Git 仓库更新工具',
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog='''示例:
  %(prog)s /path/to/projects                    # 扫描指定目录
  %(prog)s --include "*project*" --verbose     # 包含特定模式并显示详细信息
  %(prog)s --dry-run --max-workers 8          # 预览模式，8个并发线程
  %(prog)s --config /path/to/config.json       # 使用配置文件
'''
    )
    
    # 主要参数
    parser.add_argument(
        'directory', 
        nargs='?', 
        default='.', 
        help='根目录路径 (默认: 当前目录)'
    )
    
    # 包含/排除模式
    parser.add_argument(
        '--include', 
        nargs='*', 
        default=[], 
        help='包含的目录模式 (支持通配符)'
    )
    parser.add_argument(
        '--exclude', 
        nargs='*', 
        default=['*/.git', '*/venv', '*/node_modules', '*/temp'], 
        help='排除的目录模式 (默认: .git, venv, node_modules, temp)'
    )
    
    # 运行选项
    parser.add_argument(
        '--verbose', 
        '-v', 
        action='store_true', 
        help='显示详细输出'
    )
    parser.add_argument(
        '--dry-run', 
        action='store_true', 
        help='预览模式，不实际执行 git pull'
    )
    parser.add_argument(
        '--timeout', 
        type=int, 
        default=300, 
        help='命令执行超时时间(秒) (默认: 300)'
    )
    parser.add_argument(
        '--max-workers', 
        type=int, 
        default=4, 
        help='最大并发线程数 (默认: 4)'
    )
    
    # 配置选项
    parser.add_argument(
        '--config', 
        type=str, 
        help='配置文件路径 (支持 JSON 和 INI 格式)'
    )
    parser.add_argument(
        '--no-cache', 
        action='store_true', 
        help='禁用缓存功能'
    )
    
    return parser.parse_args()


def load_config_from_file(config_path: str) -> Optional[Dict[str, Any]]:
    """
    从配置文件加载配置
    
    Args:
        config_path: 配置文件路径
        
    Returns:
        Optional[Dict[str, Any]]: 配置字典，如果加载失败返回 None
    """
    if not os.path.exists(config_path):
        print(f"配置文件不存在: {config_path}")
        return None
    
    try:
        if config_path.endswith('.json'):
            with open(config_path, 'r', encoding='utf-8') as f:
                return json.load(f)
        elif config_path.endswith(('.ini', '.cfg')):
            config = configparser.ConfigParser()
            config.read(config_path, encoding='utf-8')
            return dict(config['DEFAULT'])
        else:
            print(f"不支持的配置文件格式: {config_path}")
            return None
    except Exception as e:
        print(f"加载配置文件失败: {str(e)}")
        return None


def create_config(args: argparse.Namespace, config_data: Optional[Dict[str, Any]] = None) -> Configuration:
    """
    创建配置对象，支持命令行参数和配置文件的合并
    
    Args:
        args: 命令行参数对象
        config_data: 可选的配置文件数据
        
    Returns:
        Configuration: 配置对象
    """
    try:
        # 如果有配置文件数据，先使用配置文件中的值
        if config_data:
            directory = config_data.get('directory', args.directory)
            include_patterns = config_data.get('include', args.include)
            exclude_patterns = config_data.get('exclude', args.exclude)
            verbose = config_data.get('verbose', args.verbose)
            dry_run = config_data.get('dry_run', args.dry_run)
            timeout = config_data.get('timeout', args.timeout)
            max_workers = config_data.get('max_workers', args.max_workers)
        else:
            directory = args.directory
            include_patterns = args.include
            exclude_patterns = args.exclude
            verbose = args.verbose
            dry_run = args.dry_run
            timeout = args.timeout
            max_workers = args.max_workers
        
        # 创建配置对象
        config = Configuration(
            root_directory=directory,
            include_patterns=include_patterns,
            exclude_patterns=exclude_patterns,
            verbose=verbose,
            dry_run=dry_run,
            timeout=timeout,
            max_workers=max_workers
        )
        
        if not config.is_valid:
            print(f"配置错误: {config.error_message}")
            return None
            
        return config
        
    except Exception as e:
        print(f"创建配置时发生错误: {str(e)}")
        return None


def setup_components(config: Configuration) -> Tuple[DirectoryScanner, GitExecutor, ResultReporter]:
    """
    设置和初始化组件
    
    Args:
        config: 配置对象
        
    Returns:
        tuple: (scanner, executor, reporter) 组件元组
    """
    try:
        # 创建目录扫描器
        scanner = DirectoryScanner(max_depth=10)
        
        # 创建 Git 执行器
        executor = GitExecutor(
            timeout=config.timeout,
            verbose=config.verbose
        )
        
        # 创建结果报告器
        reporter = ResultReporter(verbose=config.verbose)
        
        return scanner, executor, reporter
        
    except Exception as e:
        print(f"设置组件时发生错误: {str(e)}")
        return None, None, None


def process_repository(repo_path: str, executor: GitExecutor, reporter: ResultReporter, 
                      config: Configuration, scanner: DirectoryScanner) -> Dict[str, Any]:
    """
    处理单个 Git 仓库
    
    Args:
        repo_path: 仓库路径
        executor: Git 执行器
        reporter: 结果报告器
        config: 配置对象
        scanner: 目录扫描器
        
    Returns:
        Dict[str, Any]: 处理结果
    """
    try:
        # 检查仓库有效性
        if not scanner.is_valid_git_repo(repo_path):
            return {
                "path": repo_path,
                "status": "skipped",
                "error": "无效的 Git 仓库"
            }
        
        # 如果是预览模式，只记录跳过
        if config.dry_run:
            return {
                "path": repo_path,
                "status": "skipped",
                "error": "预览模式，未执行操作"
            }
        
        # 执行 git pull
        success, stdout, stderr = executor.pull_repository(repo_path)
        
        if success:
            return {
                "path": repo_path,
                "status": "success",
                "output": stdout.strip(),
                "error": ""
            }
        else:
            return {
                "path": repo_path,
                "status": "failed",
                "output": "",
                "error": stderr.strip()
            }
            
    except Exception as e:
        return {
            "path": repo_path,
            "status": "failed",
            "output": "",
            "error": str(e)
        }


def process_with_retry(repo_path: str, executor: GitExecutor, reporter: ResultReporter, 
                      config: Configuration, scanner: DirectoryScanner, max_retries: int = 3) -> Dict[str, Any]:
    """
    带重试机制处理仓库
    
    Args:
        repo_path: 仓库路径
        executor: Git 执行器
        reporter: 结果报告器
        config: 配置对象
        scanner: 目录扫描器
        max_retries: 最大重试次数
        
    Returns:
        Dict[str, Any]: 处理结果
    """
    import time
    
    for attempt in range(max_retries):
        try:
            # 检查仓库有效性
            if not scanner.is_valid_git_repo(repo_path):
                return {
                    "path": repo_path,
                    "status": "skipped",
                    "error": "无效的 Git 仓库"
                }
            
            # 如果是预览模式，只记录跳过
            if config.dry_run:
                return {
                    "path": repo_path,
                    "status": "skipped",
                    "error": "预览模式，未执行操作"
                }
            
            # 执行 git pull
            success, stdout, stderr = executor.pull_repository(repo_path)
            
            if success:
                return {
                    "path": repo_path,
                    "status": "success",
                    "output": stdout.strip(),
                    "error": ""
                }
            else:
                if attempt == max_retries - 1:  # 最后一次尝试
                    return {
                        "path": repo_path,
                        "status": "failed",
                        "output": "",
                        "error": stderr.strip()
                    }
                else:
                    # 指数退避重试
                    wait_time = 2 ** attempt
                    if config.verbose:
                        print(f"仓库 {repo_path} 失败，{wait_time}秒后重试 (尝试 {attempt + 1}/{max_retries})")
                    time.sleep(wait_time)
                    
        except Exception as e:
            if attempt == max_retries - 1:  # 最后一次尝试
                return {
                    "path": repo_path,
                    "status": "failed",
                    "output": "",
                    "error": str(e)
                }
            else:
                # 指数退避重试
                wait_time = 2 ** attempt
                if config.verbose:
                    print(f"仓库 {repo_path} 异常，{wait_time}秒后重试 (尝试 {attempt + 1}/{max_retries})")
                time.sleep(wait_time)
    
    # 如果所有重试都失败
    return {
        "path": repo_path,
        "status": "failed",
        "output": "",
        "error": "重试次数超限"
    }


def is_safe_git_command(command: List[str]) -> bool:
    """
    检查 Git 命令是否安全
    
    Args:
        command: Git 命令列表
        
    Returns:
        bool: 是否安全
    """
    # 允许的 Git 命令列表
    allowed_commands = {
        'pull', 'status', 'fetch', 'log', 'diff', 'show',
        'branch', 'checkout', 'merge', 'rebase', 'reset'
    }
    
    if not command or command[0] not in allowed_commands:
        return False
    
    # 检查参数安全性
    for arg in command[1:]:
        if any(char in arg for char in [';', '|', '&', '`', '$(', '${', '>']):
            return False
    
    return True


def main():
    """主执行函数"""
    # 解析命令行参数
    args = parse_args()
    
    print("Git Pull All - 批量 Git 仓库更新工具")
    print("=" * 50)
    
    # 加载配置文件（如果指定）
    config_data = None
    if args.config:
        config_data = load_config_from_file(args.config)
        if config_data:
            print(f"已加载配置文件: {args.config}")
        else:
            print("警告：配置文件加载失败，将使用默认配置")
    
    # 创建配置
    config = create_config(args, config_data)
    if not config:
        print("配置创建失败，程序退出")
        sys.exit(1)
    
    print(f"配置完成: {config.get_summary()}")
    
    # 设置组件
    scanner, executor, reporter = setup_components(config)
    if None in (scanner, executor, reporter):
        print("组件设置失败，程序退出")
        sys.exit(1)
    
    # 扫描 Git 仓库
    print(f"开始扫描目录: {config.root_directory}")
    git_repos = scanner.find_git_repositories(
        config.root_directory,
        config.include_patterns,
        config.exclude_patterns
    )
    
    if not git_repos:
        print("未找到任何 Git 仓库")
        return
    
    print(f"找到 {len(git_repos)} 个 Git 仓库")
    
    # 开始处理
    reporter.start_processing(len(git_repos))
    
    # 动态调整线程池大小
    import os
    max_workers = min(max(1, os.cpu_count() // 2), config.max_workers)
    if config.verbose:
        print(f"使用 {max_workers} 个并发线程")
    
    # 使用线程池并发处理
    results = []
    with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor_pool:
        future_to_repo = {
            executor_pool.submit(process_with_retry, repo_path, executor, reporter, config, scanner): repo_path
            for repo_path in git_repos
        }
        
        for i, future in enumerate(concurrent.futures.as_completed(future_to_repo), 1):
            repo_path = future_to_repo[future]
            try:
                result = future.result()
                results.append(result)
                reporter.log_result(
                    result["path"],
                    result["status"],
                    result.get("output", ""),
                    result.get("error", "")
                )
            except Exception as e:
                error_result = {
                    "path": repo_path,
                    "status": "failed",
                    "output": "",
                    "error": str(e)
                }
                results.append(error_result)
                reporter.log_result(repo_path, "failed", "", str(e))
    
    # 打印汇总报告
    reporter.print_summary()


if __name__ == "__main__":
    main()
